<template>
  <div class="m-groovy-editor" :style="[{ height: height + 'px' }]">
    <div ref="editor" class="CodeMirror"></div>
    <div
      class="codemirror-tipbox"
      :style="[
        { left: posLeft + 'px' },
        { top: posTop + 'px' },
        { display: tipShow ? 'inline-block' : 'none' },
      ]"
    >
      <ul class="cm-field-ul">
        <li
          v-for="(item, index) in list"
          :key="index"
          @click="handleClick(item.value)"
          :class="index == 0 ? 'cm-active cm-field-li' : 'cm-field-li'"
        >
          {{ item.value }}
        </li>
      </ul>
    </div>
  </div>
</template>
<script>
import CodeMirror from "codemirror";
import "codemirror/mode/groovy/groovy";
import "codemirror/addon/selection/active-line";
import "codemirror/addon/edit/matchbrackets";
import "codemirror/addon/fold/foldcode";
import "codemirror/addon/fold/foldgutter";
import "codemirror/addon/fold/brace-fold";
import "codemirror/addon/fold/indent-fold";
import "codemirror/addon/fold/comment-fold";
import "codemirror/lib/codemirror.css";
import "codemirror/addon/fold/foldgutter.css";
import "codemirror/theme/neo.css";
import "codemirror/theme/material.css";


// 维护说明，在线编辑代码技术请参考 CodeMirror

export default {
  props: {
    // 源码
    code: {
      type: String,
      // default: "",
      default: "",
    },

    // 提示框高度
    toolHeight: {
      type: Number,
      default: 300,
    },

    // 编辑区域大小
    height: {
      type: Number,
      default: 300,
    },

    // 关键词
    keywords: {
      type: Array,
      default() {
        return ["RUN", "SWITCH", "WAIT", "CASE", "THEN", "TIMEOUT"];
      },
    },

    // 参数名
    variables: {
      type: Array,
      default() {
        return ["预约会议"];
      },
    },
  },

  data() {
    return {
      // 当前焦点
      blurFlag: false,
      // 显示提示
      tipShow: false,
      // 下拉位置
      posLeft: 0,
      posTop: 0,
      // 下拉显示的关键词
      list: [],

      // 状态值
      state: {},

      // 编辑器
      editor: null,

      // 是否正在运行
      running: false,

      // 所有提示语
      allHints: [],
    };
  },

  mounted() {
    this.running = true;
    const that = this;

    // 将关键词加载allHints中
    this.keywords.forEach((item) => {
      this.allHints.push({
        value: item,
        type: "keyword",
      });
    });

    this.variables.forEach((item) => {
      this.allHints.push({
        value: item,
        type: "variable",
      });
    });

    CodeMirror.defineMode("simplemode", () => {
      return {
        /**
         * 初始mode的状态
         */
        startState() {
          return {
            state: "start",
          };
        },

        /**
         * 样式
         */
        token(stream, state) {
          // console.log("token", stream, state);

          if (state.state == "start") {
            // if (stream.match(/[\{\[\(]/)) {
            //   // TODO 行折叠开启
            // }

            // if (stream.match(/[\}\]\)]/)) {
            //   // TODO 行折叠关闭
            // }

            let tokenResult = null;
            // 字符串
            if (stream.match(/"(?:[^\\]|\\.)*?(?:"|$)/)) {
              tokenResult = "string";
              return tokenResult;
            }

            // 关键词
            if (stream.match(/(function)(\s+)([a-z$][\w$]*)/)) {
              tokenResult = ["keyword", null, "variable-2"];
              return tokenResult;
            }

            // if (
            //   stream.match(
            //     /(?:function|var|return|if|for|while|else|do|this)\b/
            //   )
            // ) {
            //   tokenResult = "keyword";
            //   return tokenResult;
            // }

            // 关键词
            for (let k in that.keywords) {
              if (stream.match(that.keywords[k])) {
                // console.log("token", "keyword");
                return "keyword";
              }
            }

            if (stream.match(/true|false|null|undefined/)) {
              tokenResult = "atom";
              return tokenResult;
            }

            if (
              stream.match(/0x[a-f\d]+|[-+]?(?:\.\d+|\d+\.?\d*)(?:e[-+]?\d+)?/i)
            ) {
              tokenResult = "number";
              return tokenResult;
            }

            if (stream.match(/\/\/.*/)) {
              tokenResult = "comment";
              return tokenResult;
            }

            if (stream.match(/#.*/)) {
              tokenResult = "comment";
              return tokenResult;
            }

            // if (stream.match(/\/(?:[^\\]|\\.)*?\//)) {
            //   tokenResult = "variable-3";
            //   return tokenResult;
            // }

            if (stream.match(/\/\*/)) {
              state.state = "comment";
              tokenResult = "comment";
              return tokenResult;
            }

            if (stream.match(/[-+\/*=<>!]+/)) {
              tokenResult = "operator";
              return tokenResult;
            }

            // 可用参数名
            for (let k in that.variables) {
              if (stream.match(that.variables[k])) {
                return "variable";
              }
            }

            // const result = stream.match(/[a-z$][\w$]*/);
            // console.log("result", result);
            // if (result) {
            //   tokenResult = "variable";
            //   console.log("variable");
            //   return tokenResult;
            // }

            if (!tokenResult) {
              stream.next();
            }

            return "";

            // return tokenResult;
          }

          if (state.state == "comment") {
            let tokenResult = null;
            if (stream.match(/.*?\*\//)) {
              tokenResult = "comment";
              state.state = "start";
              return tokenResult;
            }
            if (stream.match(/.*/)) {
              tokenResult = "comment";
            }
            return tokenResult;
          }
          return null;
        },

        /**
         * 此方法没有用
         */
        indent(state, textAfter, cx) {
          // console.log("indent", state, textAfter, cx);
        },
      };
    });
    //   // The start state contains the rules that are initially used
    //   // start: [
    //   //   // The regex matches the token, the token property contains the type
    //   //   { regex: /"(?:[^\\]|\\.)*?(?:"|$)/, token: "string" },
    //   //   // You can match multiple tokens at once. Note that the captured
    //   //   // groups must span the whole string in this case
    //   //   {
    //   //     regex: /(function)(\s+)([a-z$][\w$]*)/,
    //   //     token: ["keyword", null, "variable-2"],
    //   //   },
    //   //   // Rules are matched in the order in which they appear, so there is
    //   //   // no ambiguity between this one and the one above
    //   //   {
    //   //     regex: /(?:function|var|return|if|for|while|else|do|this)\b/,
    //   //     token: "keyword",
    //   //   },
    //   //   { regex: /true|false|null|undefined/, token: "atom" },
    //   //   {
    //   //     regex: /0x[a-f\d]+|[-+]?(?:\.\d+|\d+\.?\d*)(?:e[-+]?\d+)?/i,
    //   //     token: "number",
    //   //   },
    //   //   { regex: /\/\/.*/, token: "comment" },
    //   //   { regex: /\/(?:[^\\]|\\.)*?\//, token: "variable-3" },
    //   //   // A next property will cause the mode to move to a different state
    //   //   { regex: /\/\*/, token: "comment", next: "comment" },
    //   //   { regex: /[-+\/*=<>!]+/, token: "operator" },
    //   //   // indent and dedent properties guide autoindentation
    //   //   { regex: /[\{\[\(]/, indent: true },
    //   //   { regex: /[\}\]\)]/, dedent: true },
    //   //   { regex: /[a-z$][\w$]*/, token: "variable" },
    //   //   // You can embed other modes with the mode property. This rule
    //   //   // causes all code between << and >> to be highlighted with the XML
    //   //   // mode.
    //   //   { regex: /<</, token: "meta", mode: { spec: "xml", end: />>/ } },
    //   // ],
    //   // // The multi-line comment state.
    //   // comment: [
    //   //   { regex: /.*?\*\//, token: "comment", next: "start" },
    //   //   { regex: /.*/, token: "comment" },
    //   // ],
    //   // // The meta property contains global information about the mode. It
    //   // // can contain properties like lineComment, which are supported by
    //   // // all modes, and also directives like dontIndentStates, which are
    //   // // specific to simple modes.
    //   // meta: {
    //   //   dontIndentStates: ["comment"],
    //   //   lineComment: "//",
    //   // },
    // });

    // const editor = CodeMirror.fromTextArea(this.$refs["editor"], {
    const editor = CodeMirror(this.$refs["editor"], {
      // mode: "text/x-groovy",
      mode: "simplemode",
      smartIndent: true,
      // theme: theme === "night" ? "material" : "neo",
      value: this.code,
      theme: "material",
      lineNumbers: true,
      matchBrackets: true,
      styleActiveLine: false,
      foldGutter: false,
      gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"],
      readOnly: false,
      extraKeys: {
        Tab: function (cm) {
          var spaces = Array(cm.getOption("indentUnit") + 1).join(" ");
          cm.replaceSelection(spaces);
        },
      },
    });

    editor.setValue(this.code);
    editor.on("cursorActivity", (cm) => {
      // console.log("cursorActivity", cm);
      this.cursorActivity(cm);
    });
    editor.on("changes", (cm) => {
      // console.log("mirror changes", cm);
      // if (this.props.onChange) {
      // this.props.onChange(cm.getValue());
      // }
    });
    editor.on("focus", (cm) => {
      this.cursorActivity(cm);
      // this.setState({ blurFlag: true });
    });

    editor.addKeyMap({
      // 上下左右控制光标位置
      Up: (cm) => {
        if (this.tipShow) {
          this.enterFuc("up");
        } else {
          cm.execCommand("goLineUp");
        }
      },
      Down: (cm) => {
        if (this.tipShow) {
          this.enterFuc("down");
        } else {
          cm.execCommand("goLineDown");
        }
      },
      Enter: (cm) => {
        if (this.tipShow) {
          this.enterFuc("enter");
        } else {
          cm.execCommand("newlineAndIndent");
        }
      },
    });

    this.editor = editor;
    window.addEventListener("click", this.listenner);

    // 延时刷新编辑器，用于修复bug
    // 参考 https://github.com/codemirror/codemirror5/issues/3527
    setTimeout(this.refreshEditor, 200);
  },

  unmounted() {
    this.running = false;
    window.removeEventListener("click", this.listenner);
  },

  methods: {
    refreshEditor() {
      this.editor.refresh();
    },

    listenner(e) {
      const targetClassName = e.target.className;
      if (typeof targetClassName !== "string") return;
      const list = ["codemirror-tipbox"];
      const returnFalse = list.find((item) => targetClassName.includes(item));
      if (returnFalse) return false;
      const targetPath = e.path;
      let flag = false;
      targetPath.forEach((item) => {
        if (item.className) {
          if (typeof item.className !== "string") return;
          if (
            item.className.includes("CodeMirror-line") ||
            item.className.includes("CodeMirror-linenumber")
          ) {
            flag = true;
          }
        }
      });
      if (flag) {
        this.blurFlag = true;
      } else {
        this.blurFlag = false;
        this.tipShow = false;
      }
      if (targetClassName === "CodeMirror-scroll") {
        this.blurFlag = true;
      }
    },

    cursorActivity(cm) {
      // const { readOnly } = this.props;
      // if (readOnly) return;
      //
      const getCursor = cm.getCursor();
      const pos = cm.cursorCoords(getCursor);
      const getLineInfo = cm.getLine(getCursor.line);
      const cursorBeforeOneChar = getLineInfo.substring(0, getCursor.ch);
      let lastIndex = cursorBeforeOneChar.lastIndexOf(" ", getCursor.ch);
      let content = cursorBeforeOneChar.substring(lastIndex + 1, getCursor.ch);
      // const findObj = this.keywords.find((item) =>
      //   item.toLowerCase().includes(content.toLowerCase())
      // );
      let findObj = this.allHints.find((item) => {
        const value = item.value;
        return value.toLowerCase().includes(content.toLowerCase());
      });

      if (!findObj) {
        lastIndex = cursorBeforeOneChar.lastIndexOf(".", getCursor.ch);
        content = cursorBeforeOneChar.substring(lastIndex + 1, getCursor.ch);
        findObj = this.allHints.find((item) => {
          const value = item.value;
          return value.toLowerCase().includes(content.toLowerCase());
        });
      }

      // console.log("cursorActivity content2", getCursor.ch);
      if (findObj && content) {
        this.posLeft = pos.left;
        this.posTop = pos.top + 20;
        this.tipShow = true;
        this.search(content);
      } else {
        this.tipShow = false;
      }
    },

    search(val) {
      let arr = [];
      // this.keywords.forEach((item) => {
      //   if (item.toLowerCase().includes(val.toLowerCase())) {
      //     arr.push(item);
      //   }
      // });
      this.allHints.forEach((item) => {
        const value = item.value;
        if (value.toLowerCase().includes(val.toLowerCase())) {
          arr.push(item);
        }
      });
      this.list = arr;
      this.defaultFirst(val, arr);
    },

    defaultFirst(val, list) {
      let findLi = "cm-field-li";
      let active = "cm-active";
      const nodeList = document.querySelectorAll(`.${findLi}`);
      if (nodeList.length > 0) {
        for (let i = 0; i < nodeList.length; i++) {
          nodeList[i].className = findLi;
        }
        nodeList[0].className = `${active} ${findLi}`;
      }
      if (list && list.length === 1 && list[0].value === val) {
        this.tipShow = false;
      }
    },

    enterFuc(type) {
      let findLi = "cm-field-li";
      let active = "cm-active";
      const nodeList = document.querySelectorAll(`.${findLi}`);
      const length = nodeList.length;
      let index = 0;
      for (let i = 0; i < length; i++) {
        if (nodeList[i].className.includes(active)) {
          index = i;
        }
      }
      if (type === "up") {
        nodeList[index].className = findLi;
        if (index === 0) {
          nodeList[index].className = `${active} ${findLi}`;
        } else {
          nodeList[index - 1].className = `${active} ${findLi}`;
        }
      } else if (type === "down") {
        nodeList[index].className = findLi;
        if (index === length - 1) {
          nodeList[index].className = `${active} ${findLi}`;
        } else {
          nodeList[index + 1].className = `${active} ${findLi}`;
        }
      } else if (type === "enter") {
        // 点击回车时，模拟鼠标点击，并且隐藏下拉
        const node = document.querySelector(`.${active}`);
        this.handleClick(node.innerText);
        setTimeout(() => {
          this.tipShow = false;
        }, 100);
      }
      document.querySelector(`.${active}`).scrollIntoViewIfNeeded();
    },

    handleClick(item) {
      const getCursor = this.editor.getCursor();
      const getLineInfo = this.editor.getLine(getCursor.line);
      const cursorBeforeOneChar = getLineInfo.substring(0, getCursor.ch);
      const spaceLastIndex = cursorBeforeOneChar.lastIndexOf(" ", getCursor.ch);
      const dotLastIndex = cursorBeforeOneChar.lastIndexOf(".", getCursor.ch);
      const lastIndex = Math.max(spaceLastIndex, dotLastIndex);
      this.editor.setSelection(
        { line: getCursor.line, ch: lastIndex + 1 },
        { line: getCursor.line, ch: getCursor.ch }
      );
      this.editor.replaceSelection(item);
      this.editor.setCursor(getCursor.line, lastIndex + 1 + item.length);
      this.editor.focus();
      this.tipShow = false;
    },
  },
};
</script>
<style lang="less">
.m-groovy-editor {
  position: relative;
  overflow: hidden;
  .codemirror-tipbox {
    position: fixed;
    left: 0;
    top: 0;
    z-index: 999;
    background: #fff;
    width: 200px;
    box-shadow: rgba(119, 119, 119, 0.2) 0px 0px 7px,
      rgba(0, 0, 0, 0) 1px 1px 0px inset, rgba(0, 0, 0, 0) -1px -1px 0px inset;
    font-size: 12px;
    ul {
      margin: 0;
      padding: 0;
      max-height: 226px;
      overflow: auto;
      li {
        list-style: none;
        padding: 5px 10px;
        cursor: pointer;
        &:hover {
          background: #63acff;
          color: #fff;
        }
      }
      .cm-active {
        background: #63acff;
        color: #fff;
      }
    }
  }
  .cm-s-neo .CodeMirror-cursor {
    border-left: 1px solid black;
    border-right: none;
    width: 0;
  }
  .CodeMirror {
    height: 100% !important;
  }
}
</style>